/* @vitest-environment node */ import { describe, it, expect, vi, beforeEach } from "vitest"; vi.mock("@/lib/auth/session", () => ({ getSession: vi.fn(), })); vi.mock("@/lib/db", () => ({ getDb: vi.fn(), })); vi.mock("@/models/user", () => { const USER_ROLES = Object.freeze({ BRANCH: "branch", ADMIN: "admin", SUPERADMIN: "superadmin", DEV: "dev", }); return { default: { findById: vi.fn(), findOne: vi.fn(), findByIdAndDelete: vi.fn(), }, USER_ROLES, }; }); vi.mock("bcryptjs", () => { const hash = vi.fn(); return { default: { hash }, hash, }; }); vi.mock("@/lib/auth/adminTempPassword", () => ({ generateAdminTemporaryPassword: vi.fn(), })); import { getSession } from "@/lib/auth/session"; import { getDb } from "@/lib/db"; import User from "@/models/user"; import { hash as bcryptHash } from "bcryptjs"; import { generateAdminTemporaryPassword } from "@/lib/auth/adminTempPassword"; import { PATCH, POST, DELETE, dynamic } from "./route.js"; function createRequestStub(body) { return { async json() { return body; }, }; } describe("PATCH /api/admin/users/[userId]", () => { beforeEach(() => { vi.clearAllMocks(); getDb.mockResolvedValue({}); }); it('exports dynamic="force-dynamic"', () => { expect(dynamic).toBe("force-dynamic"); }); it("returns 401 when unauthenticated", async () => { getSession.mockResolvedValue(null); const res = await PATCH(createRequestStub({}), { params: Promise.resolve({ userId: "507f1f77bcf86cd799439011" }), }); expect(res.status).toBe(401); expect(await res.json()).toEqual({ error: { message: "Unauthorized", code: "AUTH_UNAUTHENTICATED" }, }); }); it("returns 403 when authenticated but not allowed (admin)", async () => { getSession.mockResolvedValue({ userId: "u1", role: "admin", branchId: null, email: "admin@example.com", }); const res = await PATCH(createRequestStub({ email: "x@y.de" }), { params: Promise.resolve({ userId: "507f1f77bcf86cd799439011" }), }); expect(res.status).toBe(403); expect(await res.json()).toEqual({ error: { message: "Forbidden", code: "AUTH_FORBIDDEN_USER_MANAGEMENT", }, }); }); it("returns 400 when JSON parsing fails", async () => { getSession.mockResolvedValue({ userId: "u2", role: "superadmin", branchId: null, email: "superadmin@example.com", }); const req = { json: vi.fn().mockRejectedValue(new Error("invalid json")) }; const res = await PATCH(req, { params: Promise.resolve({ userId: "507f1f77bcf86cd799439011" }), }); expect(res.status).toBe(400); expect(await res.json()).toEqual({ error: { message: "Invalid request body", code: "VALIDATION_INVALID_JSON", }, }); }); it("returns 400 when body is not an object", async () => { getSession.mockResolvedValue({ userId: "u2", role: "superadmin", branchId: null, email: "superadmin@example.com", }); const res = await PATCH(createRequestStub("nope"), { params: Promise.resolve({ userId: "507f1f77bcf86cd799439011" }), }); expect(res.status).toBe(400); expect(await res.json()).toEqual({ error: { message: "Invalid request body", code: "VALIDATION_INVALID_BODY", }, }); }); it("returns 400 when userId param is missing", async () => { getSession.mockResolvedValue({ userId: "u2", role: "dev", branchId: null, email: "dev@example.com", }); const res = await PATCH(createRequestStub({ email: "x@y.de" }), { params: Promise.resolve({ userId: undefined }), }); expect(res.status).toBe(400); expect(await res.json()).toEqual({ error: { message: "Missing required route parameter(s)", code: "VALIDATION_MISSING_PARAM", details: { params: ["userId"] }, }, }); }); it("returns 400 when userId param is invalid", async () => { getSession.mockResolvedValue({ userId: "u2", role: "dev", branchId: null, email: "dev@example.com", }); const res = await PATCH(createRequestStub({ email: "x@y.de" }), { params: Promise.resolve({ userId: "nope" }), }); expect(res.status).toBe(400); expect(await res.json()).toMatchObject({ error: { code: "VALIDATION_INVALID_FIELD" }, }); }); it("returns 404 when user does not exist", async () => { getSession.mockResolvedValue({ userId: "u2", role: "superadmin", branchId: null, email: "superadmin@example.com", }); User.findById.mockReturnValue({ exec: vi.fn().mockResolvedValue(null), }); const res = await PATCH(createRequestStub({ email: "x@y.de" }), { params: Promise.resolve({ userId: "507f1f77bcf86cd799439011" }), }); expect(res.status).toBe(404); expect(await res.json()).toEqual({ error: { message: "Not found", code: "USER_NOT_FOUND", details: { userId: "507f1f77bcf86cd799439011" }, }, }); }); it("returns 400 when switching to role=branch without branchId (existing has none)", async () => { getSession.mockResolvedValue({ userId: "u2", role: "dev", branchId: null, email: "dev@example.com", }); const user = { _id: "507f1f77bcf86cd799439011", username: "x", email: "x@example.com", role: "admin", branchId: null, mustChangePassword: false, createdAt: new Date(), updatedAt: new Date(), save: vi.fn(), }; User.findById.mockReturnValue({ exec: vi.fn().mockResolvedValue(user), }); const res = await PATCH(createRequestStub({ role: "branch" }), { params: Promise.resolve({ userId: "507f1f77bcf86cd799439011" }), }); expect(res.status).toBe(400); expect(await res.json()).toEqual({ error: { message: "Missing required fields", code: "VALIDATION_MISSING_FIELD", details: { fields: ["branchId"] }, }, }); }); it("returns 200 and updates fields; clears branchId for non-branch roles", async () => { getSession.mockResolvedValue({ userId: "u2", role: "superadmin", branchId: null, email: "superadmin@example.com", }); const user = { _id: "507f1f77bcf86cd799439011", username: "olduser", email: "old@example.com", role: "branch", branchId: "NL01", mustChangePassword: true, createdAt: new Date("2026-02-01T10:00:00.000Z"), updatedAt: new Date("2026-02-02T10:00:00.000Z"), save: vi.fn().mockResolvedValue(true), }; User.findById.mockReturnValue({ exec: vi.fn().mockResolvedValue(user), }); const res = await PATCH( createRequestStub({ role: "admin", mustChangePassword: false, }), { params: Promise.resolve({ userId: "507f1f77bcf86cd799439011" }), }, ); expect(res.status).toBe(200); expect(user.role).toBe("admin"); expect(user.branchId).toBe(null); expect(user.mustChangePassword).toBe(false); expect(user.save).toHaveBeenCalledTimes(1); const body = await res.json(); expect(body).toMatchObject({ ok: true, user: { id: "507f1f77bcf86cd799439011", username: "olduser", email: "old@example.com", role: "admin", branchId: null, mustChangePassword: false, }, }); }); it("returns 400 when username is already taken by another user", async () => { getSession.mockResolvedValue({ userId: "u2", role: "dev", branchId: null, email: "dev@example.com", }); const user = { _id: "507f1f77bcf86cd799439011", username: "olduser", email: "old@example.com", role: "admin", branchId: null, mustChangePassword: false, createdAt: new Date(), updatedAt: new Date(), save: vi.fn(), }; User.findById.mockReturnValue({ exec: vi.fn().mockResolvedValue(user), }); User.findOne.mockReturnValue({ select: vi.fn().mockReturnThis(), exec: vi.fn().mockResolvedValue({ _id: "507f1f77bcf86cd799439099" }), }); const res = await PATCH(createRequestStub({ username: "TakenUser" }), { params: Promise.resolve({ userId: "507f1f77bcf86cd799439011" }), }); expect(res.status).toBe(400); const body = await res.json(); expect(body).toEqual({ error: { message: "Username already exists", code: "VALIDATION_INVALID_FIELD", details: { field: "username", value: "takenuser" }, }, }); }); }); describe("POST /api/admin/users/[userId]", () => { beforeEach(() => { vi.clearAllMocks(); getDb.mockResolvedValue({}); }); it("returns 401 when unauthenticated", async () => { getSession.mockResolvedValue(null); const res = await POST(new Request("http://localhost/api/admin/users/x"), { params: Promise.resolve({ userId: "507f1f77bcf86cd799439011" }), }); expect(res.status).toBe(401); expect(await res.json()).toEqual({ error: { message: "Unauthorized", code: "AUTH_UNAUTHENTICATED" }, }); }); it("returns 403 when authenticated but not allowed (admin)", async () => { getSession.mockResolvedValue({ userId: "u1", role: "admin", branchId: null, email: "admin@example.com", }); const res = await POST(new Request("http://localhost/api/admin/users/x"), { params: Promise.resolve({ userId: "507f1f77bcf86cd799439011" }), }); expect(res.status).toBe(403); expect(await res.json()).toEqual({ error: { message: "Forbidden", code: "AUTH_FORBIDDEN_USER_MANAGEMENT", }, }); }); it("returns 400 for invalid userId", async () => { getSession.mockResolvedValue({ userId: "u2", role: "dev", branchId: null, email: "dev@example.com", }); const res = await POST(new Request("http://localhost/api/admin/users/x"), { params: Promise.resolve({ userId: "nope" }), }); expect(res.status).toBe(400); expect(await res.json()).toMatchObject({ error: { code: "VALIDATION_INVALID_FIELD" }, }); }); it("returns 400 when trying to reset the current user password", async () => { getSession.mockResolvedValue({ userId: "507f1f77bcf86cd799439011", role: "superadmin", branchId: null, email: "superadmin@example.com", }); const res = await POST(new Request("http://localhost/api/admin/users/x"), { params: Promise.resolve({ userId: "507f1f77bcf86cd799439011" }), }); expect(res.status).toBe(400); expect(await res.json()).toEqual({ error: { message: "Cannot reset current user password", code: "VALIDATION_INVALID_FIELD", details: { field: "userId", reason: "SELF_PASSWORD_RESET_FORBIDDEN" }, }, }); }); it("returns 404 when user does not exist", async () => { getSession.mockResolvedValue({ userId: "u2", role: "dev", branchId: null, email: "dev@example.com", }); User.findById.mockReturnValue({ exec: vi.fn().mockResolvedValue(null), }); const res = await POST(new Request("http://localhost/api/admin/users/x"), { params: Promise.resolve({ userId: "507f1f77bcf86cd799439099" }), }); expect(res.status).toBe(404); expect(await res.json()).toEqual({ error: { message: "Not found", code: "USER_NOT_FOUND", details: { userId: "507f1f77bcf86cd799439099" }, }, }); }); it("returns 200 and resets password with mustChangePassword=true", async () => { getSession.mockResolvedValue({ userId: "u2", role: "superadmin", branchId: null, email: "superadmin@example.com", }); const user = { _id: "507f1f77bcf86cd799439099", username: "branch2", email: "branch2@example.com", role: "branch", branchId: "NL02", passwordHash: "old-hash", mustChangePassword: false, passwordResetToken: "token", passwordResetExpiresAt: new Date("2030-01-01"), createdAt: new Date("2026-02-01T10:00:00.000Z"), updatedAt: new Date("2026-02-02T10:00:00.000Z"), save: vi.fn().mockResolvedValue(true), }; User.findById.mockReturnValue({ exec: vi.fn().mockResolvedValue(user), }); generateAdminTemporaryPassword.mockReturnValue("TempPass123!"); bcryptHash.mockResolvedValue("hashed-temp"); const res = await POST(new Request("http://localhost/api/admin/users/x"), { params: Promise.resolve({ userId: "507f1f77bcf86cd799439099" }), }); expect(res.status).toBe(200); expect(generateAdminTemporaryPassword).toHaveBeenCalledTimes(1); expect(bcryptHash).toHaveBeenCalledWith("TempPass123!", 12); expect(user.passwordHash).toBe("hashed-temp"); expect(user.mustChangePassword).toBe(true); expect(user.passwordResetToken).toBe(null); expect(user.passwordResetExpiresAt).toBe(null); expect(user.save).toHaveBeenCalledTimes(1); expect(await res.json()).toMatchObject({ ok: true, temporaryPassword: "TempPass123!", user: { id: "507f1f77bcf86cd799439099", username: "branch2", email: "branch2@example.com", role: "branch", branchId: "NL02", mustChangePassword: true, }, }); }); }); describe("DELETE /api/admin/users/[userId]", () => { beforeEach(() => { vi.clearAllMocks(); getDb.mockResolvedValue({}); }); it("returns 401 when unauthenticated", async () => { getSession.mockResolvedValue(null); const res = await DELETE( new Request("http://localhost/api/admin/users/x"), { params: Promise.resolve({ userId: "507f1f77bcf86cd799439011" }), }, ); expect(res.status).toBe(401); expect(await res.json()).toEqual({ error: { message: "Unauthorized", code: "AUTH_UNAUTHENTICATED" }, }); }); it("returns 403 when authenticated but not allowed (admin)", async () => { getSession.mockResolvedValue({ userId: "u1", role: "admin", branchId: null, email: "admin@example.com", }); const res = await DELETE( new Request("http://localhost/api/admin/users/x"), { params: Promise.resolve({ userId: "507f1f77bcf86cd799439011" }), }, ); expect(res.status).toBe(403); expect(await res.json()).toEqual({ error: { message: "Forbidden", code: "AUTH_FORBIDDEN_USER_MANAGEMENT", }, }); expect(User.findByIdAndDelete).not.toHaveBeenCalled(); }); it("returns 400 for invalid userId", async () => { getSession.mockResolvedValue({ userId: "u2", role: "dev", branchId: null, email: "dev@example.com", }); const res = await DELETE( new Request("http://localhost/api/admin/users/x"), { params: Promise.resolve({ userId: "nope" }), }, ); expect(res.status).toBe(400); expect(await res.json()).toMatchObject({ error: { code: "VALIDATION_INVALID_FIELD" }, }); }); it("returns 400 when trying to delete the current user (self delete)", async () => { getSession.mockResolvedValue({ userId: "507f1f77bcf86cd799439011", role: "superadmin", branchId: null, email: "superadmin@example.com", }); const res = await DELETE( new Request("http://localhost/api/admin/users/x"), { params: Promise.resolve({ userId: "507f1f77bcf86cd799439011" }), }, ); expect(res.status).toBe(400); expect(await res.json()).toEqual({ error: { message: "Cannot delete current user", code: "VALIDATION_INVALID_FIELD", details: { field: "userId", reason: "SELF_DELETE_FORBIDDEN" }, }, }); expect(User.findByIdAndDelete).not.toHaveBeenCalled(); }); it("returns 404 when user does not exist", async () => { getSession.mockResolvedValue({ userId: "u2", role: "dev", branchId: null, email: "dev@example.com", }); User.findByIdAndDelete.mockReturnValue({ select: vi.fn().mockReturnThis(), exec: vi.fn().mockResolvedValue(null), }); const res = await DELETE( new Request("http://localhost/api/admin/users/x"), { params: Promise.resolve({ userId: "507f1f77bcf86cd799439099" }), }, ); expect(res.status).toBe(404); expect(await res.json()).toEqual({ error: { message: "Not found", code: "USER_NOT_FOUND", details: { userId: "507f1f77bcf86cd799439099" }, }, }); }); it("returns 200 and deleted user payload on success", async () => { getSession.mockResolvedValue({ userId: "u2", role: "superadmin", branchId: null, email: "superadmin@example.com", }); User.findByIdAndDelete.mockReturnValue({ select: vi.fn().mockReturnThis(), exec: vi.fn().mockResolvedValue({ _id: "507f1f77bcf86cd799439099", username: "todelete", email: "todelete@example.com", role: "branch", branchId: "NL01", mustChangePassword: true, createdAt: new Date("2026-02-01T10:00:00.000Z"), updatedAt: new Date("2026-02-02T10:00:00.000Z"), }), }); const res = await DELETE( new Request("http://localhost/api/admin/users/x"), { params: Promise.resolve({ userId: "507f1f77bcf86cd799439099" }), }, ); expect(res.status).toBe(200); const body = await res.json(); expect(body).toMatchObject({ ok: true, user: { id: "507f1f77bcf86cd799439099", username: "todelete", email: "todelete@example.com", role: "branch", branchId: "NL01", mustChangePassword: true, }, }); }); });